Skip to content

fix(webui): unwrap untrusted-content envelope client-side, escape on render - #118

Merged
jkyberneees merged 1 commit into
mainfrom
fix/webui-client-side-sanitization
Jul 27, 2026
Merged

fix(webui): unwrap untrusted-content envelope client-side, escape on render#118
jkyberneees merged 1 commit into
mainfrom
fix/webui-client-side-sanitization

Conversation

@jkyberneees

Copy link
Copy Markdown
Contributor

Summary

Standardizes on client-side sanitization for odek serve: the server sends all WebSocket message content raw and unsanitized (it already did — no server changes), and the WebUI now handles the model-facing <untrusted_content_<nonce> source="..."> envelope correctly instead of rendering the escaped tag text plus the escaped tool body.

What was wrong

  • Tool results displayed the literal envelope text (&lt;untrusted_content_a1b2c3 source="shell"&gt;) along with the escaped body — the envelope is model-facing trust metadata, not user content.
  • Session-history replay of user messages with attachments dumped the escaped wrapper and full file body; stripAttachmentBodies targeted a stale --- name (size) --- format that no longer matches the server's source="attachment:<name>" envelope.
  • Genuine double-escape: addMessage/renderAssistantMessage called markdownToHtml(escapeHtml(content)) even though markdownToHtml escapes all input by default, so history bubbles rendered &lt; literally while the live stream rendered <.

Changes

  • ui/js/untrusted.js (new): envelope parser with nonce-backreferenced regex mirroring cmd/odek/untrusted.go; forged/mismatched nonces are treated as plain text. Exports parseUntrusted, unwrapUntrusted, hasUntrustedWrapper, stripAttachmentBodies.
  • ui/js/render.js: tool results unwrap the envelope and render via textContent only (no more innerHTML/data-full for bodies); the envelope source shows as a 🔒 <source> badge. Shared by live and history paths. Double-escape removed in addMessage/renderAssistantMessage.
  • ui/js/utils.js: stripAttachmentBodies re-exported from untrusted.js (pure, node-testable).
  • ui/style.css: .tb-source badge styling.
  • ui/js/untrusted.test.js (new): 11 node:test cases — parsing, nonce-mismatch forgery, attachment chips, escaped render chain, double-escape regression pin.
  • docs/WEBUI.md: new Content sanitization contract section for third-party/TUI clients (server sends raw; clients MUST escape; envelope SHOULD be unwrapped for display), corrected protocol table (tool_call/tool_result use data, full untruncated output; added thinking, subagent_log, skill_event, memory_event, agent_signal, approval_ack), updated stale frontend description.

Testing

  • node --test cmd/odek/ui/js/*.test.js — 35/35 pass
  • go build ./... + go test ./cmd/odek/ -count=1 — pass
  • Not covered: manual browser smoke test (needs a live odek serve session with a model)

…render

Standardize on client-side sanitization for odek serve: the server sends
all WebSocket content raw and unsanitized (it already did); the WebUI now
handles the model-facing <untrusted_content_*> envelope properly instead
of displaying the escaped tag text.

- ui/js/untrusted.js: nonce-backreferenced envelope parser (parseUntrusted,
  unwrapUntrusted, hasUntrustedWrapper, stripAttachmentBodies) mirroring the
  grammar of cmd/odek/untrusted.go; forged/mismatched nonces pass through as
  plain text
- ui/js/render.js: tool results unwrap the envelope and render via
  textContent only (no innerHTML/data-full for bodies); envelope source
  shows as a badge. Fixes live + history paths via shared helper. Also drops
  the redundant escapeHtml() under markdownToHtml in addMessage and
  renderAssistantMessage (markdownToHtml escapes all input by default), so
  history and live stream render identically
- ui/js/utils.js: stripAttachmentBodies re-exported from untrusted.js;
  attachment envelopes collapse to chip placeholders again (stale
  '--- name (size) ---' format replaced by source="attachment:<name>")
- ui/style.css: .tb-source badge styling
- ui/js/untrusted.test.js: node:test regression suite incl. double-escape
  and escaped-render-chain contract pins
- docs/WEBUI.md: content sanitization contract for third-party clients,
  corrected WS protocol table (tool_call/tool_result data fields, missing
  event types), updated frontend description
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
odek 4a18724 Commit Preview URL

Branch Preview URL
Jul 27 2026, 10:39 AM

@jkyberneees
jkyberneees merged commit 9a0c0fa into main Jul 27, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant